/*
 * Copyright © 2021 camunda services GmbH (info@camunda.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.zeebe.containers.archive;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
import org.testcontainers.containers.GenericContainer;

/**
 * Represents a TAR file on a remote container, which can be extracted to a given path. If the TAR
 * was a directory, the destination path is expected to be a directory as well. This can be
 * particularly useful if you want to debug the data/state generated by Zeebe, or want to use it as
 * a starting point for later tests.
 *
 * <p>Copying anything from a container to your host will cause Docker to produce a TAR archive with
 * the contents that were requested. Unfortunately, we can't use that directly since Zeebe's data
 * contains hard-links, and those will be broken by the standard Docker copy command. Instead, the
 * data has to be TAR'd beforehand on the container with the right options (e.g. following
 * hard-links), and then it will be extracted. This means the raw output of the COPY command will be
 * essentially a TAR file which contains a TAR file. This overhead can be removed when we can
 * specify the flags for TAR that the COPY command will use.
 *
 * <p>By default, instances of this class are just references to an existing archive on the
 * container. If you want to create the archive from a path, consider using the builder. If the
 * archive already exists however, then you should use the constructor directly.
 *
 * <p>Example usage:
 *
 * <pre>@{code
 *   void extractData() {
 *     // configure and start your container
 *     final ZeebeBrokerContainer container = new ZeebeBrokerContainer();
 *     container.start();
 *
 *     // generate some actual data...
 *     // extract it to a given destination
 *     final Path destination = Paths.of("/tmp/extractedData");
 *     final ContainerArchive archive = ContainerArchive.builder().withContainer(container).build();
 *     archive.extract(destination);
 *   }
 * }</pre>
 */
@API(status = Status.EXPERIMENTAL)
public final class ContainerArchive {
  private final String archivePath;
  private final GenericContainer<?> container;

  /**
   * Creates a new reference to a TAR file at path {@code archivePath} on the given container.
   *
   * <p>NOTE: this simply creates a reference to the archive. If you wish to also create the archive
   * * from a path, then use the {@link #builder()}.
   *
   * @param archivePath the path of the archive on the container
   * @param container the container on which the archive resides
   */
  public ContainerArchive(final String archivePath, final GenericContainer<?> container) {
    this.archivePath = archivePath;
    this.container = container;
  }

  /**
   * Returns a builder for a remote container archive. This is the recommended way of creating
   * instances of this class, as it will use some sane defaults and simplify the creation of the
   * archive for you.
   *
   * <p>If you want to reference a pre-existing archive however, then consider using the constructor
   * directly.
   *
   * @return a builder to create and reference a remote container archive
   */
  public static ContainerArchiveBuilder builder() {
    return new ContainerArchiveBuilder();
  }

  /**
   * Extracts the remote TAR file to the given {@code destination}. If the contents of the TAR file
   * is a file, then destination should not exist.
   *
   * <p>This method will ensure that any directories missing in the {@code destination} path are
   * also created via {@link java.nio.file.Files#createDirectories(Path, FileAttribute[])}.
   *
   * @param destination the destination path for the archive's contents
   */
  public void extract(final Path destination) {
    container.copyFileFromContainer(
        archivePath, rawInput -> extractFromInput(destination, rawInput));
  }

  /**
   * Transfers the archive to the local host at the given {@code destination}.
   *
   * <p>This method will ensure that any directories missing in the {@code destination} path are
   * also created via {@link java.nio.file.Files#createDirectories(Path, FileAttribute[])}.
   *
   * @param destination the path on the local host to save the archive at
   * @throws IOException if the archive cannot be read on the container, or the destination path is
   *     not writable
   */
  public void transferTo(final Path destination) throws IOException {
    Files.createDirectories(destination.getParent());
    container.copyFileFromContainer(archivePath, input -> Files.copy(input, destination));
  }

  private Void extractFromInput(final Path destination, final InputStream rawInput)
      throws IOException {
    try (final GzipCompressorInputStream gzipInput = new GzipCompressorInputStream(rawInput);
        final TarArchiveInputStream tarInput = new TarArchiveInputStream(gzipInput)) {
      TarExtractor.INSTANCE.extract(tarInput, destination);
    }

    return null;
  }
}
